Skip to content

fix(service-automation): 从未 seal 的引擎首次执行 flow 时告警一次 (#4792) - #5595

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4792-seal-warn-once
Aug 5, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-4792-seal-warn-once

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4792

前提核对(对当前 origin/main)

issue 写于 08-03,先核对现状再动手,前提成立:

  • packages/services/service-automation/src/engine.ts:1132 — private nodeTypeVocabularySealed = false 仍在,字段名未变;
  • engine.ts:3600 sealNodeTypeVocabulary() 是唯一执行 ADR-0018 节点类型校验的时刻;engine.ts:2022 registerFlow 只在 已 seal 后才即时校验;
  • plugin.ts:850/878 — AutomationServicePlugin 仍在 kernel:bootstrapped 调用 seal(插件路径必然早于任何 execute(),不会误报);
  • execute() 里没有任何人加过相关告警(grep never sealed / warnOnce / hasWarned 全无命中)。

即:直接 new AutomationEngine() 且不自己 seal 的嵌入式 host,至今仍是完全静默地拿不到该校验。

改动

execute() 在通过「flow 存在 + 未被禁用」两道门之后调用新的 warnIfNodeTypeVocabularyNeverSealed():词汇表未 seal 且本引擎还没说过,则打一条 warn,写明丢了什么(ADR-0018 校验从未运行,这些节点改为运行中 NO_EXECUTOR 失败)和怎么修(host 在插件贡献完执行器后调用 engine.sealNodeTypeVocabulary();插件路径在 kernel:bootstrapped 做这件事)。等级按 #4632 判为功能性降级 → warn。

三个设计决定,都有代码证据:

  1. 去重按引擎实例(nodeTypeSealOmissionWarned 是实例字段,不是模块级):按租户/环境各建一个引擎的 host 是在每个引擎上都漏了这次调用,模块级 flag 只会报第一个跑起来的那个。
  2. 只报「缺了这次调用」,不报未知类型审计结果。 未 seal 的引擎按契约词汇表仍可增长,在那里断言「某类型没有执行器」正是 AGENTS.md「Startup registry reads」禁止的形态 —— 会被本次启动反驳、又已经写进日志收不回来,等于把 bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 在嵌入式路径上重建一遍。「host 没调用过 seal」则是关于 host 的事实,后续任何注册都改不了它。需要审计结果又不想封闭词汇表的 host 用只读的 getUnknownNodeTypeAudit()。
  3. 不顺带自动 seal(issue 留给接手者的二选一)。除了 issue 自己说的「两个答案」坏味道,代码证据更硬:engine.ts:2022 显示 seal 之后 registerFlow 转为即时校验,所以自动 seal 会让「先执行 flow、后注册插件执行器」的嵌入式 host(ADR-0018 允许词汇表开放,这完全合法)开始收到 bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 那种「will fail at execution time」误报 —— 那正是 bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 要删的东西。seal 的所有权语义因此未被改变(仍由 host 决定),不构成公共契约分叉,故按代码证据裁定而非上抛。

告警文本刻意避开 warnUnknownNodeTypes 的 no registered executor or descriptor 字样:多处测试与日志过滤用这个子串计数「每 flow 的未知类型发现」,一条只是在谈论它们的行文不能被算成一条(第一版就撞上了,flow-node-type-audit.test.ts 的计数器同样读这个子串)。

消费半径与 fixture 处置

execute() 的调用者遍及全仓,因此按规则的消费半径扫 fixture,而不是只扫被改的包:全仓 new AutomationEngine( 的 51 处里,生产代码只有 plugin.ts(走 seal),其余全是测试。本包整套跑完暴露出 builtin/decision-branch-routing.test.ts 的 5 个用例 —— 它们是嵌入式 host,expect(warnings).toHaveLength(0) 表达的是「#4414 路由没有告警」。

处置选的是「补声明」而不是「过滤断言」:两处 harness 在注册完执行器/flow 后补 engine.sealNodeTypeVocabulary(),让 fixture 真正表达一个装配正确的 host,零容忍断言得以原样保留。若改成把新告警从断言里过滤掉,以后任何借用这个 harness 的测试都会连带对真实信号失明。

测试与反向验证

反向验证的方向是先预测再跑:

实验 预测 实际
删掉 execute() 里的调用(还原修复前) 3 个「期望有告警」的嵌入式用例红;2 个哨兵与 decision-branch-routing 保持绿 一致:Tests 3 failed / 18 passed
把 seal 判断取反(已 seal 才告警) 两条哨兵红(证明它们守的是 seal 状态而非字符串);补了 seal 的 5 个 routing 用例也应转红 一致:Tests 10 failed / 11 passed,含两条 SENTINEL 与 5 个 routing

第二个实验同时证明我给 decision-branch-routing.test.ts 补的 seal 是承重的,不是装饰。注意第 4 个新用例(「未注册/已禁用的 flow 名不触发」)断言的是「无告警」,所以在实验一里理应保持绿 —— 它确实绿,这与「修前红」的模板预期不同,如实记录。

跑过的命令(真实输出摘要):

  • pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 → Test Files 59 passed (59) / Tests 713 passed (713)(707 旧 + 6 新)
  • 消费半径:plugin-approvals 446 / trigger-schedule 42 / trigger-record-change 55 / connector-mcp 23 / connector-rest 16 / connector-slack 8,全绿
  • 门:check:startup-registry-verdict(40 seam(s) … none recording a verdict the boot can contradict)、check:durability-log-level、check:nul-bytes、check:adr-anchors、check:type-check-coverage 全过;改动文件 eslint 无输出
  • 类型:本包在 [P2] framework: 66 个包用 tsup 构建、无人做类型检查 —— 实测 18 个包共 380 处 code-tier 错误(#4118 的 framework 侧对应) #4311 DEBT 台账内、无 typecheck script,故直接 tsc --noEmit -p tsconfig.json 核对 —— 报错全部落在我未触碰的文件(engine.test.ts 的 TS2739 即台账所记、nested-region-parity.test.ts 的 TS2341),新增文件与 engine.ts 零报错

changeset:patch(用户可见的新告警)。未碰 wait-node / plugin.ts 的绑定日志面。构建过程中 packages/spec/authorable-surface.base.json 被 gen:schema 重新锚定,已还原、未提交。分支基于 81087877e;其后落在 main 的两个提交(#5577 / #5578)只动 .claude/agents/os-dev.md 与 packages/client/vitest.integration.config.ts,与本改动零重叠,故未做无谓合并 —— PR CI 会校验合并结果。

Generated by Claude Code

… node-type vocabulary (#4792)

#4771 made sealNodeTypeVocabulary() the only moment the ADR-0018 node-type
check runs. AutomationServicePlugin seals at kernel:bootstrapped, so plugin
hosts are covered — but a host that constructs `new AutomationEngine()` and
never seals lost the check entirely, in silence, discoverable only by reading
a changeset. The first execute() on an unsealed engine now says so once.

- Once per engine INSTANCE, not per process: a host with one engine per
  tenant/environment omitted the call on each of them.
- The line reports the missing CALL, not the unknown-type audit: an unsealed
  engine's vocabulary can still grow by contract, so naming absent executors
  there would rebuild #4771's contradictable verdict inside the embedded path.
  getUnknownNodeTypeAudit() stays the read-only probe for hosts that want the
  findings without closing the vocabulary.
- It deliberately does NOT auto-seal: authority over "closed" stays with the
  host, and after a seal registerFlow validates inline — auto-sealing would
  hand the false "will fail at execution time" warnings to any embedded host
  that registers executors after its first run (legal under ADR-0018).

Tests: embedded host warns once; unknown/disabled flow names do not trigger
it; the warn does not seal; plus two sentinels — an explicitly-sealed host and
the AutomationServicePlugin boot gain no log line. decision-branch-routing's
harnesses now seal (they are embedded hosts asserting zero warnings about
#4414 routing), which the inversion check proves is load-bearing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 8:10pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Contributor Author

一个审阅者大概会问的边界,先说清楚:告警只挂 execute(),没有挂 resume()。

resume() 走 resumeInternal(),不经过 execute(),所以「进程重启 + 持久化 suspended-run store,新进程第一件事就是 resume(runId)」这条路径不会立刻告警 —— 要等该 host 下一次真正 execute() 才说。这是覆盖面更窄,不是判断有误:验收要的就是「首次执行」,而 resume 的前提是某个进程里曾经执行过并挂起。刻意不铺开,一是本单范围就是 execute() 首跑,二是每多一个入口就多一处需要证明「插件路径零新增日志」的哨兵。若维护者认为重启即 resume 的嵌入式部署值得覆盖,那是一个独立的小扩面单,不必阻塞本 PR。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 20:16
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit cc5b048 Aug 5, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4792-seal-warn-once branch August 5, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants